﻿
using System;
using System.Activities;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.ServiceModel;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Workflow;


namespace Fine.CRM.Common
{
    /// <summary>
    /// Base class used for all plugin development. 
    /// </summary>
    public class Plugin : IPlugin
    {
        const string ENABLE_INFO_MESSAGE_SETTINGNAME = ".TraceLog.EnableInfoMessages";
        const string PRE_IMAGE_NAME = "PreImage";

        public sealed class LocalPluginContext
        {
            internal TraceLog TraceLog { get; set; }

            internal Entity Target
            {
                get;

                private set;
            }


            internal Entity PreImage { get; private set; }

            /// <summary>
            /// Returns an Entity record with all attributes from the current Target and any additional attributes
            /// that might exist in the Pre Image entity if provided.
            /// </summary>
            internal Entity MergedTarget
            {
                get
                {
                    if (this.Target == null)
                    {
                        return null;
                    }

                    Entity mergedRecord = new Entity(this.Target.LogicalName);
                    mergedRecord.Id = this.Target.Id;
                    mergedRecord.MergeWith(this.Target);
                    mergedRecord.MergeWith(this.PreImage);
                    return mergedRecord;
                }
            }

            /// <summary>
            /// Provides access to cached name/value settings stored in the Fine Solutions Extension Settings entity.
            /// </summary>
            internal ExtensionSettings ExtensionSettings { get; private set; }

            /// <summary>
            /// Provides access to cached configuration data stored in Data(xml) web resources within the CRM organization.
            /// </summary>
            internal XmlConfigurationResource XmlConfigurationResources { get; private set; }

            internal IServiceProvider ServiceProvider
            {
                get;

                private set;
            }

            internal IOrganizationService OrganizationService
            {
                get;

                private set;
            }

            internal IPluginExecutionContext PluginExecutionContext
            {
                get;

                private set;
            }

            internal ITracingService TracingService
            {
                get;

                private set;
            }

            private LocalPluginContext()
            {
            }

            internal LocalPluginContext(IServiceProvider serviceProvider, string ClassName)
            {
                if (serviceProvider == null)
                {
                    throw new ArgumentNullException("serviceProvider");
                }

                string className = ClassName;

                // Obtain the execution context service from the service provider.
                this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

                // Obtain the tracing service from the service provider.
                this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

                // Obtain the Organization Service factory service from the service provider
                IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

                // Use the factory to generate the Organization Service.
                this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId);

                this.ExtensionSettings = new ExtensionSettings(this.OrganizationService, className);
                
                this.XmlConfigurationResources = new XmlConfigurationResource(this.OrganizationService);

                if (this.PluginExecutionContext.InputParameters.Contains("Target")
                    && this.PluginExecutionContext.InputParameters["Target"] is Entity)
                {
                    this.Target = (Entity)this.PluginExecutionContext.InputParameters["Target"];
                }

                if (this.PluginExecutionContext.PreEntityImages.Contains(PRE_IMAGE_NAME))
                {
                    this.PreImage = this.PluginExecutionContext.PreEntityImages[PRE_IMAGE_NAME];
                }
            }

            internal void Trace(string message)
            {
                if (string.IsNullOrWhiteSpace(message) || this.TracingService == null)
                {
                    return;
                }

                if (this.PluginExecutionContext == null)
                {
                    this.TracingService.Trace(message);
                }
                else
                {
                    this.TracingService.Trace(
                        "{0}, Correlation Id: {1}, Initiating User: {2}",
                        message,
                        this.PluginExecutionContext.CorrelationId,
                        this.PluginExecutionContext.InitiatingUserId);
                }

                if (this.TraceLog == null || this.TraceLog.EnableInformationMessages == false)
                {
                    return;
                }

                this.TraceLog.Trace(message);

            }
        }

        private Collection<Tuple<int, string, string, Action<LocalPluginContext>>> registeredEvents;

        /// <summary>
        /// Gets the List of events that the plug-in should fire for. Each List
        /// Item is a <see cref="System.Tuple"/> containing the Pipeline Stage, Message and (optionally) the Primary Entity. 
        /// In addition, the fourth parameter provide the delegate to invoke on a matching registration.
        /// </summary>
        protected Collection<Tuple<int, string, string, Action<LocalPluginContext>>> RegisteredEvents
        {
            get
            {
                if (this.registeredEvents == null)
                {
                    this.registeredEvents = new Collection<Tuple<int, string, string, Action<LocalPluginContext>>>();
                }

                return this.registeredEvents;
            }
        }

        /// <summary>
        /// Gets or sets the name of the child class.
        /// </summary>
        /// <value>The name of the child class.</value>
        protected string ChildClassName
        {
            get;

            private set;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Plugin"/> class.
        /// </summary>
        /// <param name="childClassName">The <see cref=" cred="Type"/> of the derived class.</param>
        internal Plugin(Type childClassName)
        {
            this.ChildClassName = childClassName.ToString();
        }

        /// <summary>
        /// Executes the plug-in.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <remarks>
        /// For improved performance, Microsoft Dynamics CRM caches plug-in instances. 
        /// The plug-in's Execute method should be written to be stateless as the constructor 
        /// is not called for every invocation of the plug-in. Also, multiple system threads 
        /// could execute the plug-in at the same time. All per invocation state information 
        /// is stored in the context. This means that you should not use global variables in plug-ins.
        /// </remarks>
        public void Execute(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
            {
                throw new ArgumentNullException("serviceProvider");
            }

            // Construct the Local plug-in context.
            LocalPluginContext localcontext = new LocalPluginContext(serviceProvider,this.ChildClassName);

            localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName));

            //get debug setting for this class
            bool enableInfoMessages = localcontext.ExtensionSettings.Get<bool>(ENABLE_INFO_MESSAGE_SETTINGNAME, false);

            //setup the trace log
            localcontext.TraceLog = new TraceLog(
                localcontext.OrganizationService,
                localcontext.PluginExecutionContext.CorrelationId.ToString(),
                localcontext.PluginExecutionContext.PrimaryEntityName,
                localcontext.PluginExecutionContext.MessageName,
                this.ChildClassName,
                new EntityReference("systemuser", localcontext.PluginExecutionContext.InitiatingUserId),
                enableInfoMessages);

            localcontext.TraceLog.TracingService = localcontext.TracingService;

            try
            {
                // Iterate over all of the expected registered events to ensure that the plugin
                // has been invoked by an expected event
                // For any given plug-in event at an instance in time, we would expect at most 1 result to match.
                Action<LocalPluginContext> entityAction =
                    (from a in this.RegisteredEvents
                     where (
                     a.Item1 == localcontext.PluginExecutionContext.Stage &&
                     a.Item2 == localcontext.PluginExecutionContext.MessageName &&
                     (string.IsNullOrWhiteSpace(a.Item3) ? true : a.Item3 == localcontext.PluginExecutionContext.PrimaryEntityName)
                     )
                     select a.Item4).FirstOrDefault();

                if (entityAction != null)
                {
                    //log start of execution.
                    localcontext.TraceLog.Trace(string.Format(
                        CultureInfo.InvariantCulture,
                        "{0} is firing for Entity: {1}, Message: {2}",
                        this.ChildClassName,
                        localcontext.PluginExecutionContext.PrimaryEntityName,
                        localcontext.PluginExecutionContext.MessageName),
                        "Execute",
                        TraceLog.MessageType.Information);

                    entityAction.Invoke(localcontext);

                    return;
                }
            }
            catch (FaultException<OrganizationServiceFault> e)
            {
                localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString()));

                // Handle the exception.
                throw;
            }
            finally
            {
                localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName));

                if (localcontext.TraceLog != null)
                {
                    localcontext.TraceLog.Save();
                }

            }
        }
    }

    public class WorkflowActivity : CodeActivity
    {
        const string ENABLE_INFO_MESSAGE_SETTINGNAME = ".TraceLog.EnableInfoMessages";

        protected class LocalWorkflowContext
        {
            internal TraceLog TraceLog { get; set; }

            internal Entity Target
            {
                get;

                private set;
            }

            /// <summary>
            /// Provides access to cached name/value settings stored in the Fine Solutions Extension Settings entity.
            /// </summary>
            internal ExtensionSettings ExtensionSettings { get; private set; }

            /// <summary>
            /// Provides access to cached configuration data stored in Data(xml) web resources within the CRM organization.
            /// </summary>
            internal XmlConfigurationResource XmlConfigurationResources { get; private set; }

            internal IOrganizationService OrganizationService
            {
                get;

                private set;
            }

            internal CodeActivityContext ExecutionContext
            {
                get;

                private set;

            }

            internal IWorkflowContext WorkflowContext
            {
                get;

                private set;
            }

            internal ITracingService TracingService
            {
                get;

                private set;
            }

            private LocalWorkflowContext()
            {
            }

            internal LocalWorkflowContext(CodeActivityContext executionContext, string className)
            {
                if (executionContext == null)
                {
                    throw new ArgumentNullException("executionContext");
                }

                this.ExecutionContext = executionContext;

                this.WorkflowContext = executionContext.GetExtension<IWorkflowContext>();
                IOrganizationServiceFactory factory = executionContext.GetExtension<IOrganizationServiceFactory>();
                OrganizationService = factory.CreateOrganizationService(WorkflowContext.UserId);
                this.TracingService = executionContext.GetExtension<ITracingService>();

                this.ExtensionSettings = new ExtensionSettings(OrganizationService,className);
                this.XmlConfigurationResources = new XmlConfigurationResource(this.OrganizationService);
                
                if (WorkflowContext.InputParameters.Contains("Target") && WorkflowContext.InputParameters["Target"] is Entity)
                {
                    this.Target = (Entity)WorkflowContext.InputParameters["Target"];
                }

                //get debug setting for this class
                
                bool enableInfoMessages = this.ExtensionSettings.Get<bool>(ENABLE_INFO_MESSAGE_SETTINGNAME, false);

                //setup the trace log
                this.TraceLog = new TraceLog(
                    this.OrganizationService,
                    this.WorkflowContext.CorrelationId.ToString(),
                    this.WorkflowContext.PrimaryEntityName,
                    this.WorkflowContext.MessageName,
                    className,
                    new EntityReference("systemuser", this.WorkflowContext.InitiatingUserId),
                    enableInfoMessages);

                this.TraceLog.TracingService = this.TracingService;

            }

            internal void Trace(string message)
            {
                if (string.IsNullOrWhiteSpace(message) || this.TracingService == null)
                {
                    return;
                }

                if (this.WorkflowContext == null)
                {
                    this.TraceLog.Trace(message);
                }
                else
                {
                    this.TraceLog.Trace(string.Format(
                        "{0}, Correlation Id: {1}, Initiating User: {2}",
                        message,
                        this.WorkflowContext.CorrelationId,
                        this.WorkflowContext.InitiatingUserId));
                }
            }
        }

        private Collection<Action<LocalWorkflowContext>> registeredEvents;

        /// <summary>
        /// Gets the List of events that the plug-in should fire for. Each List
        /// Item is a <see cref="System.Tuple"/> containing the Pipeline Stage, Message and (optionally) the Primary Entity. 
        /// In addition, the fourth parameter provide the delegate to invoke on a matching registration.
        /// </summary>
        protected Collection<Action<LocalWorkflowContext>> RegisteredEvents
        {
            get
            {
                if (this.registeredEvents == null)
                {
                    this.registeredEvents = new Collection<Action<LocalWorkflowContext>>();
                }

                return this.registeredEvents;
            }
        }

        /// <summary>
        /// Gets or sets the name of the child class.
        /// </summary>
        /// <value>The name of the child class.</value>
        protected string ChildClassName
        {
            get;

            private set;
        }

        internal WorkflowActivity() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Plugin"/> class.
        /// </summary>
        /// <param name="childClassName">The <see cref=" cred="Type"/> of the derived class.</param>
        internal WorkflowActivity(Type childClassName)
        {
            this.ChildClassName = childClassName.ToString();
        }


        protected override void Execute(CodeActivityContext executionContext)
        {
            if (executionContext == null)
            {
                throw new ArgumentNullException("executionContext");
            }

            // Construct the Local workflow context.
            LocalWorkflowContext localcontext = new LocalWorkflowContext(executionContext, this.ChildClassName);

            localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName));


            try
            {
                foreach (Action<LocalWorkflowContext> entityAction in this.registeredEvents)
                {
                    //log start of execution.
                    localcontext.TraceLog.Trace(string.Format(
                        CultureInfo.InvariantCulture,
                        "{0} is firing for Entity: {1}, Message: {2}",
                        this.ChildClassName,
                        localcontext.WorkflowContext.PrimaryEntityName,
                        localcontext.WorkflowContext.MessageName),
                        "Execute",
                        TraceLog.MessageType.Information);

                    entityAction.Invoke(localcontext);
                }



            }
            catch (Exception e)
            {
                localcontext.TraceLog.Trace(string.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString()), TraceLog.MessageType.Exception);

                // Handle the exception.
                throw;
            }
            finally
            {
                localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName));

                if (localcontext.TraceLog != null)
                {
                    localcontext.TraceLog.Save();
                }
            }
        }


    }
}